Skip to content

[cognitiveservices] Unblock managed compute creation - fix misleading 202 / AuthorizationFailed errors #48096

Merged
msyyc merged 7 commits into
Azure:mainfrom
swarupecenits:fix/cognitiveservices-compute-create-no-poll
Jul 22, 2026
Merged

[cognitiveservices] Unblock managed compute creation - fix misleading 202 / AuthorizationFailed errors #48096
msyyc merged 7 commits into
Azure:mainfrom
swarupecenits:fix/cognitiveservices-compute-create-no-poll

Conversation

@swarupecenits

@swarupecenits swarupecenits commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

Fixes the misleading errors users hit when creating a Cognitive Services managed compute via
ComputesOperations.begin_create_or_update (surfaced through az cognitiveservices account compute create).

The generated SDK failed asynchronous compute creation in two ways:

  1. Rejected the async 202 Accepted. The service returns HTTP 202 for the async create, but the
    generated _create_or_update_initial only accepts 200/201, so it raised
    Operation returned an invalid status 'Accepted'.
  2. Polled a permission-gated endpoint. The default poller followed the Azure-AsyncOperation header
    to .../locations/{location}/computeOperations/{operationId}, which requires
    Microsoft.CognitiveServices/locations/computeOperations/read. Callers allowed to create a compute
    but not granted that read permission saw a misleading AuthorizationFailed even though the create
    succeeded.

What this change does

Overrides ComputesOperations.begin_create_or_update (sync + async, in _patch.py) to:

  • Accept 202 in addition to 200/201 (in a minimal _create_or_update_initial override that is
    otherwise identical to the generated method).
  • Track the long-running operation by reading provisioningState from the list API via a custom
    blocking poller (_ComputeListPolling / _AsyncComputeListPolling), matching the compute by name.
    This needs only computes/read (which callers already have) and never touches the
    computeOperations operation-status endpoint
    .
  • Block until the compute reaches a terminal state and, on failure, surface the compute's own
    properties.errors
    (e.g. the quota message) instead of a generic
    Operation returned an invalid status 'OK'.
  • Preserve the generated method's public shape: the three @overloads, the @api_version_validation
    guard, the polling=False escape hatch, and the continuation_token resume path (which does not
    re-issue the create PUT). Genuine non-2xx create failures still propagate.

Why list and not a resource get

The compute create is asynchronous, and a GET on the just-created compute returns
404 "Cluster not found" for an extended, variable period while the cluster backend materializes — long
enough that a get-based poller wrongly fails a create that is actually succeeding. The compute does,
however, appear in the list response with its provisioningState from the moment the create is
accepted. Polling list therefore reflects the true state throughout provisioning. This also matches the
work item's guidance to "rely on the list API for status polling of compute creation."

Only private helpers were added (_ComputeListPolling, _AsyncComputeListPolling, _provisioning_error,
_state_of, _TERMINAL_FAILED_STATES, _TERMINAL_SUCCESS_STATES); the public API surface is
unchanged
(verified: the three public overloads are preserved and api-version validation still fires).

Notes

  • This is a client-side workaround while the service-side polling API is fixed; it lives in _patch.py
    so the code generator does not overwrite it.
  • _patch.py re-implements the initial request send only because the generated _create_or_update_initial
    rejects the 202; it is kept intentionally close to the generated code.
  • The poller blocks until terminal (standard LRO poller.result() semantics) rather than returning early,
    and surfaces provisioning failures - so callers get correct results, not a premature "Accepted".
  • Version bumped 15.0.0b315.0.0b4; CHANGELOG updated.

Testing

  • 22 unit tests (tests/test_computes_operations_patch.py + ..._async.py, 11 sync + 11 async):
    accept 202 · reads status via list, never computeOperations · blocks until terminal · tolerates the
    compute being briefly absent from list then appearing · gives up (bounded) if it never appears ·
    surfaces the real failure reason · propagates non-2xx · polling=False · public overloads preserved ·
    old api-version rejected · continuation_token does not re-send the create. All pass; black +
    pyflakes clean.
  • Live-verified against a real account (jayant-westcentralus, Standard_D64_v3):
    • Success path: create accepted (202), poller blocked through ~33 min of provisioning reading
      provisioningState from list (never a 404), and returned the compute with
      provisioningState: Succeeded.
    • Failure path: on an over-quota create, the poller blocked to terminal and raised the service's
      real error (ClusterCoreQuotaReached quota message) instead of a generic status error.
    • Invalid / non-full-node SKUs are rejected up front, and no AuthorizationFailed /
      invalid status 'Accepted' occurs.

All SDK Contribution checklist:

  • The pull request does not introduce [breaking changes]
  • CHANGELOG is updated for new features, bug fixes or other significant changes.
  • I have read the contribution guidelines.

General Guidelines and Best Practices

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

Verification

**Unit tests - all passing. **
test_computes_operations_patch.py (sync) and test_computes_operations_patch_async.py (async) cover accepting the async 202, polling the compute resource instead of computeOperations, blocking until terminal state, tolerating the read-after-write 404 window, surfacing the real provisioning failure, propagating non-2xx errors, and the polling=False escape hatch.


Screenshot 2026-07-17 182240

Live end-to-end success. begin_create_or_update(...).result() accepts the async 202, tracks
status via the list API (STEP 2: computeOperations never called), blocks through ~33 min of
provisioning, and returns the created Standard_D64_v3 compute with provisioningState: Succeeded

  • no AuthorizationFailed, no invalid status 'Accepted', and none of the 404 "Cluster not found"
    that a get-based poller hits during creation.

Screenshot 2026-07-17 170329 Screenshot 2026-07-17 170509

…lerate read-after-write 404

Override begin_create_or_update (sync + async) to accept the service's HTTP 202 and track the LRO by polling the compute resource (BodyContentPolling) instead of the computeOperations status endpoint that needs computeOperations/read many callers lack. Tolerates the ~15-20s read-after-write 404 window, blocks until terminal, and surfaces the compute's own provisioning error. Adds 14 unit tests; bumps to 15.0.0b4.
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
9 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

This comment was marked as resolved.

@swarupecenits swarupecenits changed the title  [cognitiveservices] Unblock managed compute creation — fix misleading 202 / AuthorizationFailed errors (Task 5441745) [cognitiveservices] Unblock managed compute creation — fix misleading 202 / AuthorizationFailed errors (Task 5441745) Jul 16, 2026
…, no computeOperations)

Poll provisioningState from the list API instead of a resource GET: a GET on a just-created compute returns 404 'Cluster not found' for an extended period while it provisions, whereas list reflects state from the moment the create is accepted. Keeps the 202 accept, blocks until terminal via a custom _ComputeListPolling, surfaces the compute's real error on failure, and preserves the public overloads, api-version validation and continuation_token resume path. Updates 22 unit tests and the CHANGELOG.
@swarupecenits
swarupecenits marked this pull request as ready for review July 17, 2026 11:43
Copilot AI review requested due to automatic review settings July 17, 2026 11:43
@swarupecenits
swarupecenits requested a review from msyyc as a code owner July 17, 2026 11:43
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
9 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 17, 2026 12:07

This comment was marked as resolved.

…idate continuation_token

- Apply caller-supplied cls to the final resource in the list-based poller.
- Preserve terminal Canceled status instead of collapsing it to Failed.
- Encode/decode continuation_token so resume uses the token as source of truth and raises on a malformed token.
- Add sync+async tests for all three (30 tests total).
Copilot AI review requested due to automatic review settings July 17, 2026 12:55

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 17, 2026 13:21

This comment was marked as resolved.

@swarupecenits swarupecenits changed the title [cognitiveservices] Unblock managed compute creation — fix misleading 202 / AuthorizationFailed errors (Task 5441745) [cognitiveservices] Unblock managed compute creation - fix misleading 202 / AuthorizationFailed errors Jul 17, 2026
@msyyc

msyyc commented Jul 20, 2026

Copy link
Copy Markdown
Member

We will discuss about this PR in e-mail.

Comment thread sdk/cognitiveservices/azure-mgmt-cognitiveservices/CHANGELOG.md Outdated
Co-authored-by: Yuchao Yan <yuchaoyan@microsoft.com>
Copilot AI review requested due to automatic review settings July 21, 2026 17:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (6)

sdk/cognitiveservices/azure-mgmt-cognitiveservices/azure/mgmt/cognitiveservices/operations/_patch.py:473

  • The continuation token is decoded as this new list-poller format before the caller-supplied PollingMethod is selected. Tokens are owned by their polling method—LROPoller.from_continuation_token delegates to polling_method.from_continuation_token—so this rejects every custom poller's token and breaks the existing resume contract. Decode the scope only for the default list poller and otherwise pass the token through untouched.
            resource_group_name, account_name, compute_name = _decode_continuation_token(cont_token)

sdk/cognitiveservices/azure-mgmt-cognitiveservices/azure/mgmt/cognitiveservices/aio/operations/_patch.py:387

  • The continuation token is decoded as this new list-poller format before the caller-supplied AsyncPollingMethod is selected. AsyncLROPoller.from_continuation_token delegates decoding to the selected polling method, so this rejects custom async poller tokens and regresses resumability. Decode only when using the default list poller; custom-method tokens must pass through unchanged.
            resource_group_name, account_name, compute_name = _decode_continuation_token(cont_token)

sdk/cognitiveservices/azure-mgmt-cognitiveservices/azure/mgmt/cognitiveservices/operations/_patch.py:223

  • Passing None as the first cls argument does not preserve the generated hook contract: callers may inspect the final PipelineResponse for status, headers, or context. The added test only validates the deserialized argument, so such callbacks now fail with an attribute error. Capture the response associated with the final list read and pass that response to cls.
        if self._resource is not None and self._cls is not None:
            return self._cls(None, self._resource, {})

sdk/cognitiveservices/azure-mgmt-cognitiveservices/azure/mgmt/cognitiveservices/aio/operations/_patch.py:137

  • Passing None as the first cls argument breaks the generated async hook contract for callbacks that inspect the final PipelineResponse. The test only checks the deserialized value, masking this regression. Retain the response from the final async list page and provide it to cls.
        if self._resource is not None and self._cls is not None:
            return self._cls(None, self._resource, {})

sdk/cognitiveservices/azure-mgmt-cognitiveservices/api.md:1182

  • The PR states that the public API surface is unchanged, but the regenerated snapshot now publishes a private _ComputesOperationsGenerated base class. This is an observable inheritance/API change rather than the prior base-free declaration. Keep the implementation base out of the public declaration (or obtain API approval for the change) and regenerate the snapshot.
    class azure.mgmt.cognitiveservices.aio.operations.ComputesOperations(_ComputesOperationsGenerated):

sdk/cognitiveservices/azure-mgmt-cognitiveservices/api.md:9526

  • The PR claims the public API is unchanged, but this snapshot exposes the new private _ComputesOperationsGenerated base in the sync public class. That changes the documented inheritance surface. Avoid replacing the public class with a subclass (or obtain API approval for this change), then regenerate the API artifacts.
    class azure.mgmt.cognitiveservices.operations.ComputesOperations(_ComputesOperationsGenerated):

saanikaguptamicrosoft added a commit to saanikaguptamicrosoft/azure-cli that referenced this pull request Jul 21, 2026
…5.0.0b3 to 15.0.0b4

Adopts the SDK release that includes Swarup's fix (Azure/azure-sdk-for-python#48096) for compute create polling. End-to-end validated in a local editable-install of 15.0.0b4: request accepted, polling correctly hits the LIST endpoint, real provisioning errors surface (no more misleading AuthorizationFailed on computeOperations/read).

CI will fail on dependency resolution until 15.0.0b4 is published to PyPI (same pre-publish precedent as PR Azure#33132). Once the SDK is on PyPI, this CLI PR is ready to merge without re-review.
@saanikaguptamicrosoft

Copy link
Copy Markdown
Member

Good job, @swarupecenits !
As a follow-up, we will update typespec for having 202 as an expected status code, and fixing the position of location kwarg. So that in future when we remove this patch changes once LRO logic is fixed at service-side, it doesn't break the create flow.

@saanikaguptamicrosoft

Copy link
Copy Markdown
Member

Also tested this PR from CLI: Azure/azure-cli#33759
Works well for all 4 compute cluster operations.

Copilot AI review requested due to automatic review settings July 21, 2026 18:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

sdk/cognitiveservices/azure-mgmt-cognitiveservices/azure/mgmt/cognitiveservices/operations/_patch.py:223

  • The generated cls contract requires a real PipelineResponse as its first argument (_operations.py:43, and the original LRO callback passes the final response at lines 20543-20547). Passing None breaks valid callbacks that inspect pipeline_response.http_response whenever default list polling completes. Preserve an actual response from the list read-back (or otherwise retain the callback contract) instead of substituting None.
            return self._cls(None, self._resource, {})

sdk/cognitiveservices/azure-mgmt-cognitiveservices/azure/mgmt/cognitiveservices/aio/operations/_patch.py:137

  • The async generated cls contract also requires a real PipelineResponse as its first argument. Passing None makes supported callbacks that inspect pipeline_response.http_response fail after polling succeeds. Capture and pass the response associated with the final list read-back rather than weakening the public callback contract.
            return self._cls(None, self._resource, {})

sdk/cognitiveservices/azure-mgmt-cognitiveservices/api.md:1182

  • This snapshot shows that the async public API surface did change: ComputesOperations now exposes the private _ComputesOperationsGenerated base, contrary to the PR's unchanged-surface claim. Adjust the customization so the private implementation base is not part of the public class signature, then regenerate the API snapshot and metadata.
    class azure.mgmt.cognitiveservices.aio.operations.ComputesOperations(_ComputesOperationsGenerated):

sdk/cognitiveservices/azure-mgmt-cognitiveservices/api.md:9526

  • The sync API snapshot likewise exposes _ComputesOperationsGenerated as a new public base class, so the public surface is not unchanged as described. Avoid leaking the private implementation base through ComputesOperations, and regenerate the API artifacts after correcting the class customization.
    class azure.mgmt.cognitiveservices.operations.ComputesOperations(_ComputesOperationsGenerated):

@msyyc
msyyc merged commit db5dd77 into Azure:main Jul 22, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants